﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

namespace DokiDokiDynamo {


    /// <summary>
    /// 3Dグラフィックス
    /// </summary>
    /// <remarks>
    /// <see cref="Graphics3D"/>はシーンを描画するグラフィックデバイス依存のクラスです。
    /// ユーザーはこのクラスの唯一のインスタンスを使用してシーンを描画します。
    /// 方法については(未定)
    /// </remarks>
    public class Graphics3D {

        #region Field
        static Graphics3D g3d = new Graphics3D ();
        static CacheArray<int> glCache = new CacheArray<int> ();

        List<Texture> renderTargets;
        bool colorClearEnabled;
        uint clearColor;
        bool depthClearEnabled;
        float clearDepth;
        Func<World, IEnumerable<Node>> selection;
        int[] viewport;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        public Graphics3D () {
            this.renderTargets = new List<Texture> (MaxRenderTarget);
            this.colorClearEnabled = true;
            this.clearColor = 0xff7f7f7f;
            this.depthClearEnabled = true;
            this.clearDepth = 1.0f;
            this.selection = null;
            this.viewport = new int[] { 0, 0, 0, 0 };
        }

        /// <summary>
        /// スタティック･コンストラクター
        /// </summary>
        static Graphics3D () {
            glCache.ReleaseEvents += new CacheArray<int>.ReleaseHandler (glCache_ReleaseEvents);
        }


        #endregion

        #region Events
        /// <summary>
        /// レンダリングパス・イベント
        /// </summary>
        /// <remarks>
        /// このイベントは<see cref="RenderPassHandler"/>型のメソッドを受け取り、
        /// いわゆるレンダリングパスを設定します。
        /// DDDでは固有のレンダリングパスをエンジン内部で実装せず、
        /// すべて後からユーザーがこのイベントを通して注入します。
        /// </remarks>
        public event RenderPassHandler RenderPass;
        #endregion

        #region Property

        /// <summary>
        /// ビューポート
        /// </summary>
        public int[] Viewport {
            get { return (int[])viewport.Clone (); }
        }

        /// <summary>
        /// ノードセレクション
        /// </summary>
        /// <remarks>
        /// シーンを描画するときにノードをある条件にしたがって選択、ソートする事が可能です。
        /// これをノードセレクションといいます。
        /// ノードセレクションの実態は引数<see cref="World"/>を受け取り<see cref="Node"/>の列挙子を返すデリゲートです。
        /// LINQ構文によるラムダ式を想定しています。
        /// </remarks>
        public Func<World, IEnumerable<Node>> Selection {
            get { return selection; }
            set { this.selection = value; }
        }

        /// <summary>
        /// このデバイスがサポートしているレンダーターゲットの最大数
        /// </summary>
        /// <remarks>
        /// 現在の実装ではクエリーを行わず固定値４を返しているだけです。
        /// ごめんなさい。
        /// </remarks>
        public int MaxRenderTarget {
            get { return 4; }
        }

        /// <summary>
        /// レンダーターゲット
        /// </summary>
        /// <remarks>
        /// デフォルトのレンダーターゲットです。常に0番に等しいです。
        /// nullの場合はデフォルトのレンダーターゲット(通常はフレームバッファー)に描画されます。
        /// </remarks>
        public Texture RenderTarget {
            get { return renderTargets[0]; }
        }

        /// <summary>
        /// レンダーターゲット0
        /// </summary>
        /// <remarks>
        /// 0番目のレンダーターゲットです。
        /// nullの場合はデフォルトのレンダーターゲット(通常はフレームバッファー)に描画されます。
        /// </remarks>
        public Texture RenderTarget0 {
            get { return renderTargets[0]; }
            set { }
        }

        /// <summary>
        /// レンダーターゲット1
        /// </summary>
        /// <remarks>
        /// 1番目のレンダーターゲットです。
        /// </remarks>
        public Texture RenderTarget1 {
            get { return renderTargets[1]; }
            set { }
        }

        /// <summary>
        /// レンダーターゲット2
        /// </summary>
        /// <remarks>
        /// 2番目のレンダーターゲットです。
        /// </remarks>
        public Texture RenderTarget2 {
            get { return renderTargets[1]; }
            set { }
        }

        /// <summary>
        /// レンダーターゲット3
        /// </summary>
        /// <remarks>
        /// 3番目のレンダーターゲットです。
        /// </remarks>
        public Texture RenderTarget3 {
            get { return renderTargets[3]; }
            set { }
        }

        /// <summary>
        /// カラーバッファーのクリアフラグ
        /// </summary>
        /// <remarks>
        /// このプロパティがtrueの時は値<see cref="ClearColor"/>でカラーバッファーをクリアします。
        /// 複数のレンダーターゲットがセットされている場合は、すべてクリアします。
        /// falseの時は何もしません。
        /// </remarks>
        public bool IsColorClearEnabled {
            get { return colorClearEnabled; }
            set { this.colorClearEnabled = value; }
        }

        /// <summary>
        /// デプスバッファーのクリアフラグ
        /// </summary>
        /// <remarks>
        /// このプロパティがtrueの時は値<see cref="ClearDepth"/>でデプスバッファーをクリアします。
        /// falseの時は何もしません。
        /// </remarks>
        public bool IsDepthClearEnabled {
            get { return depthClearEnabled; }
            set { this.depthClearEnabled = value; }
        }

        /// <summary>
        /// クリア・カラー
        /// </summary>
        /// <remarks>
        /// カラーバッファーのクリアに使われる値です。
        /// </remarks>
        public uint ClearColor {
            get { return clearColor; }
            set { this.clearColor = value; }
        }

        /// <summary>
        /// クリア・デプス
        /// </summary>
        /// <remarks>
        /// デプスバッファーのクリアに使われる値です。
        /// </remarks>
        public float ClearDepth {
            get { return clearDepth; }
            set { this.clearDepth = value; }
        }

        #endregion

        #region Method



        /// <summary>
        /// 唯一の<see cref="Graphics3D"/>オブジェクトの取得
        /// </summary>
        /// <returns>
        /// <see cref="Graphics3D"/>はシングルトンクラスで唯一のインスタンスをこのメソッドを通して取得します。
        /// </returns>
        public static Graphics3D GetInstance () {
            return g3d;
        }

        public static CacheArray<int> GetGLCache () {
            return glCache;
        }


        public void SetViewort (int x, int y, int width, int height) {
            if (x < 0) {
                throw new ArgumentException ("X is invalid");
            }
            if (y < 0) {
                throw new ArgumentException ("Y is invalid");
            }
            if (width < 0) {
                throw new ArgumentException ("Width is invalid");
            }
            if (height < 0) {
                throw new ArgumentException ("Height is invalid");
            }

            this.viewport[0] = x;
            this.viewport[1] = y;
            this.viewport[2] = width;
            this.viewport[3] = height;
        }

        /// <summary>
        /// リセット？？？
        /// </summary>
        /// <remarks>
        /// このメソッドを定義した記憶がありません。
        /// なぜ存在するのか不明で消すのが怖いので消してません。
        /// そのうち何とかします。
        /// </remarks>
        public void Reset () {
        }

        /// <summary>
        /// シーンをレンダリングします
        /// </summary>
        /// このメソッドを呼び出すと設定した<see cref="Selection"/>に従ってノードを選択・ソートした後
        /// <see cref="RenderPass"/>を実行して規定のレンダーターゲットにシーンを描画します。
        /// <param name="wld">シーン</param>
        public void Render (World wld) {

            if (RenderPass != null) {
                foreach (var ev in RenderPass.GetInvocationList ()) {

                    // レンダーパスの読み込み
                    ev.DynamicInvoke (null);

                    // 全体設定
                    var color = System.Drawing.Color.FromArgb ((int)clearColor);
                    GL.ClearColor (color);
                    if (IsColorClearEnabled) {
                        GL.Clear (ClearBufferMask.ColorBufferBit);
                    }
                    if (IsDepthClearEnabled) {
                        GL.Clear (ClearBufferMask.DepthBufferBit);
                    }
                    GL.Viewport (viewport[0], viewport[1], viewport[2], viewport[3]);

                    var nodes = (selection == null) ? wld.Downwards : selection (wld);

                    // コンパイル･コード
                    foreach (var node in nodes.Where (x => x.Is<TransformMesh> ())) {
                        var mesh = node.As<TransformMesh> ();
                        mesh.Compile ();
                    }
                    foreach (var node in nodes.Where (x => x.Is<Mesh> ())) {
                        var mesh = node.As<Mesh>();
                        mesh.Compile ();
                    }

                    // レンダリング・コード
                    foreach (var node in nodes.Where (x => x.Is<TransformMesh> ())) {
                        var mesh = node.As<TransformMesh> ();
                        mesh.Render (node);
                    }
                    foreach (var node in nodes.Where (x => x.Is<Mesh> ())) {
                        var mesh = node.As<Mesh> ();
                        mesh.Render (node);
                    }
                }
            }
        }

        /// <summary>
        /// OpenGLリソースを開放します
        /// </summary>
        /// <remarks>
        /// manegedオブジェクトは全て自動でGCによって回収されますが､そこで使用されていたOpenGLリソースは自動では解放されません｡
        /// 通常このようなunmanagedリソースはユーザーが明示的にDispose()メソッドを呼び出して解放するのがC#の流儀ですが､
        /// DDDでは(めんどくさいので)全て自動で処理します｡
        /// ユーザーは定期的にこのメソッドを呼んで用済みになったOpenGLリソースを解放してください｡
        /// </remarks>
        public void Release () {
            glCache.Release ();
        }


        /// <summary>
        /// OpenGLリソースを開放します
        /// </summary>
        /// <remarks>
        /// このメソッドは<see cref="CacheArray{TValue}.ReleaseEvents"/>イベントにセットされ、
        /// GCによってOpenGLリソースが不要になったと判断されたときに呼び出されます。
        /// 第1引数の<paramref name="obj"/>はリソースを保有していたオブジェクトの型です(インスタンスはGCによって回収済みです)｡
        /// 第2引数はOpenGLの識別し(int)です｡
        /// </remarks>
        /// (*1) この2つのクラスはジェネリック型が継承して使うので単純な比較ではtrueにならない｡
        /// <param name="objectType">リソースを保有していたオブジェクト</param>
        /// <param name="id">OpenGLリソース</param>
        static void glCache_ReleaseEvents (Type objectType, int id) {
            if (objectType == typeof (VertexBuffer)) {
                GL.DeleteVertexArrays (1, ref id);
                return;
            }
            if (objectType == typeof (VertexShader)) {
                GL.DeleteShader (id);
                return;
            }
            if (objectType == typeof (FragmentShader)) {
                GL.DeleteShader (id);
                return;
            }
            if (objectType == typeof (ShaderProgram)) {
                GL.DeleteProgram (id);
                return;
            }
            if (objectType == typeof (Image2D)) {
                GL.DeleteTexture (id);
                return;
            }
            if (objectType == typeof (Texture)) {
                GL.DeleteSamplers (1, ref id);
                return;
            }
            if (objectType.IsSubclassOf (typeof (IndexBuffer))) {   /// (*1)
                GL.DeleteBuffers (1, ref id);
                return;
            }
            if (objectType.IsSubclassOf (typeof (VertexArray))) {   /// (*1)
                GL.DeleteBuffers (1, ref id);
                return;
            }

        }


        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>
        /// <returns>現在のオブジェクトを説明する文字列</returns>
        public override string ToString () {
            return string.Format ("Graphics3D: ColorClear={0},{1}, DepthClear={2},{3},", IsColorClearEnabled, ClearColor, IsDepthClearEnabled, ClearDepth);
        }


        #endregion

    }
}
